#pragma once
#include "Pocelain.h"
#include <limits>
#include "math/TSpherical.hpp"
POCELAIN_NAMESPACE_BEGIN
enum MOUSE
{
    LEFT = 0,
    MIDDLE = 1,
    RIGHT = 2,
    ROTATE = 0,
    DOLLY = 1,
    PAN = 2,
};

class OrbitControl : public Node3D
{
public:
    bool enabled = true;
    OrbitControl(IInput *input,Node3D *object)
        :mObject(object)
        ,mInput(input)
    {
        quat = Quaternion().setFromUnitVectors( mObject->getUp(), Vector3( 0, 1, 0 ));
        quatInverse = quat;
        quatInverse.invert();
    }

    virtual ~OrbitControl()
    {
    }

    enum class STATE : int {
        NONE= -1,
        ROTATE= 0,
        DOLLY= 1,
        PAN= 2,
        TOUCH_ROTATE= 3,
        TOUCH_PAN= 4,
        TOUCH_DOLLY_PAN= 5,
        TOUCH_DOLLY_ROTATE= 6
    }state = STATE::NONE;

    Vector3 offset;
    Vector3 panOffset;
    bool zoomChanged = false;
    Vector3 lastPosition;
    Quaternion lastQuaternion;

    Quaternion quat;
    Quaternion quatInverse;

    // current position in spherical coordinates
    Spherical spherical;
    Spherical sphericalDelta;

    const Real twoPI = MathUtils::PI * 2;
    const Real EPS = 0.000001;

private:
    Real getZoomScale()
    {
        return std::pow( 0.95, zoomSpeed );
    }

    void handleMouseDownRotate()
    {
        rotateStart = mInput->getMousePosition();
    }

    void handleMouseDownDolly()
    {
        dollyStart = mInput->getMousePosition();
    }

    void handleMouseDownPan()
    {
        panStart = mInput->getMousePosition();
    }

    void handleMouseMoveRotate()
    {
        rotateEnd = mInput->getMousePosition();
        rotateDelta.subVectors(
                       rotateEnd, rotateStart )
            .multiplyScalar(rotateSpeed );
//        auto element = scope.domElement;
        auto winSize = mInput->getWindowSize();
        rotateLeft( 2 * MathUtils::PI * rotateDelta.x / winSize.y ); // yes, height
        rotateUp( 2 * MathUtils::PI * rotateDelta.y / winSize.y );
        rotateStart = rotateEnd;
        _update();
    }

    void handleMouseMoveDolly()
    {
        dollyEnd = mInput->getMousePosition();

        dollyDelta.subVectors( dollyEnd, dollyStart );

        if ( dollyDelta.y > 0 )
        {
            dollyOut( getZoomScale() );
        }
        else if ( dollyDelta.y < 0 )
        {
            dollyIn( getZoomScale() );
        }

        dollyStart.copy( dollyEnd );

        _update();
    }

    void panLeft(Real distance,const Matrix4 &objectMatrix )
    {
        Vector3 v;
        v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix
        v.multiplyScalar( - distance );
        panOffset.add( v );
    };

    void panUp(Real distance,const Matrix4 &objectMatrix )
    {
        Vector3 v;
        if ( screenSpacePanning == true )
        {
            v.setFromMatrixColumn( objectMatrix, 1 );

        }
        else
        {
            v.setFromMatrixColumn( objectMatrix, 0 );
            v.crossVectors( mObject->getUp(), v );
        }

        v.multiplyScalar( distance );
        panOffset.add( v );
    };

    void pan(Real deltaX,Real deltaY )
    {
//        const element = scope.domElement;
        if ( mObject->isPerspectiveCamera() )
        {
            // perspective
            auto camera = dynamic_cast<PerspectiveCamera*>(mObject.get());
            auto position = camera->getPosition();
            offset.copy( position ).sub( mTarget );
            auto targetDistance = offset.length();

            // half of the fov is center to top of screen
            targetDistance *= std::tan( ( camera->getFov() / 2 ) * MathUtils::PI / 180.0 );

            // we use only clientHeight here so aspect ratio does not distort speed
            auto winSize = mInput->getWindowSize();
            panLeft( 2 * deltaX * targetDistance / winSize.y, camera->getMatrixLocal());
            panUp( 2 * deltaY * targetDistance / winSize.y, camera->getMatrixLocal());

        }
        else if ( mObject->isOrthographicCamera() )
        {
            POCELAIN_LOG_ERROR("ERROR: Not implemented");
            assert(false);
            // orthographic
//            auto camera = dynamic_cast<OrthographicCamera*>(mObject.get());
//            panLeft( deltaX * ( camera->right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix );
//            panUp( deltaY * ( camera->top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix );

        }
        else
        {
            // camera neither orthographic nor perspective
            POCELAIN_LOG_WARN("WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.");
            enablePan = false;
        }
    }
    void handleMouseMovePan()
    {
        panEnd = mInput->getMousePosition();
        panDelta.subVectors( panEnd, panStart ).multiplyScalar( panSpeed );
        pan( panDelta.x, panDelta.y );
        panStart.copy( panEnd );
        _update();
    }

    void handleMouseWheel()
    {
        auto mouseScrollDelta = mInput->getMouseScrollDelta();
        if ( mouseScrollDelta.y < 0 )
        {
            dollyIn( getZoomScale() );
        }
        else if ( mouseScrollDelta.y > 0 )
        {
            dollyOut( getZoomScale() );
        }
        _update();
    }
    void dollyOut( Real dollyScale )
    {
        if ( mObject->isPerspectiveCamera() )
        {
            scale /= dollyScale;
        }
        else if ( mObject->isOrthographicCamera() )
        {
            POCELAIN_LOG_ERROR("Not implemented");
//            scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) );
//            scope.object.updateProjectionMatrix();
//            zoomChanged = true;
            assert(false);
        }
        else
        {
            POCELAIN_LOG_WARN("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.");
            enableZoom = false;
        }

    }

    void dollyIn(Real dollyScale )
    {
        if ( mObject->isPerspectiveCamera() )
        {
            scale *= dollyScale;
        }
        else if ( mObject->isOrthographicCamera())
        {
            POCELAIN_LOG_ERROR("Not implemented");
            assert(false);
//            scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) );
//            scope.object.updateProjectionMatrix();
//            zoomChanged = true;
        }
        else
        {
            POCELAIN_LOG_WARN("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled." );
            enableZoom = false;
        }
    }
    void onMouseDown()
    {
        if(!enabled)
        {
            return;
        }
        int mouseAction;

        if(mInput->getKeyDown(EKeyCode::Mouse0))
        {
            mouseAction = mouseButtons::LEFT;
        }
        else if(mInput->getKeyDown(EKeyCode::Mouse1))
        {
            mouseAction = mouseButtons::RIGHT;
        }
        else if(mInput->getKeyDown(EKeyCode::Mouse2))
        {
            mouseAction = mouseButtons::MIDDLE;
        }
        else
        {
            mouseAction = - 1;
        }

        switch(mouseAction)
        {
        case MOUSE::DOLLY:
        {
            if ( enableZoom == false ) return;
            handleMouseDownDolly();
            state = STATE::DOLLY;
            break;
        }
        case MOUSE::ROTATE:
        {
            if (
                mInput->getKey(EKeyCode::LeftControl)
                ||mInput->getKey(EKeyCode::LeftShift)
                )
            {
                if ( enablePan == false ) return;
                handleMouseDownPan();
                state = STATE::PAN;
            }
            else
            {
                if ( enableRotate == false ) return;
                handleMouseDownRotate();
                state = STATE::ROTATE;
            }
            break;
        }
        case MOUSE::PAN:
        {
            if (
                mInput->getKey(EKeyCode::LeftControl)
                ||mInput->getKey(EKeyCode::LeftShift)
                )
            {
                if ( enableRotate == false ) return;
                handleMouseDownRotate();
                state = STATE::ROTATE;
            } else {
                if ( enablePan == false ) return;
                handleMouseDownPan();
                state = STATE::PAN;
            }
            break;
        }
        default:
        {
            state = STATE::NONE;
            break;
        }
        }
    }

    void onMouseUp()
    {
//        removePointer( event );
//        if ( pointers.length === 0 )
//        {
//            scope.domElement.releasePointerCapture( event.pointerId );
//            scope.domElement.removeEventListener( 'pointermove', onPointerMove );
//            scope.domElement.removeEventListener( 'pointerup', onPointerUp );
//        }
//        scope.dispatchEvent( _endEvent );
        state = STATE::NONE;
    }

    void onMouseWheel()
    {
        if ( enabled == false || enableZoom == false || state != STATE::NONE ) return;
//        event.preventDefault();
//        scope.dispatchEvent( _startEvent );
        handleMouseWheel();
//        dispatchEvent( _endEvent );
    }

    void onMouseMove()
    {
        switch ( state )
        {
        case STATE::ROTATE:
        {

            if ( enableRotate == false ) return;

            handleMouseMoveRotate();

            break;
        }
        case STATE::DOLLY:
        {
            if ( enableZoom == false ) return;

            handleMouseMoveDolly();

            break;
        }

        case STATE::PAN:
        {

            if ( enablePan == false ) return;

            handleMouseMovePan();

            break;
        }

        }
    }
    Real getAutoRotationAngle()
    {
        return 2 * MathUtils::PI / 60 / 60 * this->autoRotateSpeed;
    }

    void rotateLeft(Real angle )
    {
        sphericalDelta.theta -= angle;
    }

    void rotateUp(Real angle )
    {
        sphericalDelta.phi -= angle;
    }
public:
    Real scale = 1.0;

    void update(Real delta)
    {
        if(
            mInput->getKeyDown(EKeyCode::Mouse0)
            || mInput->getKeyDown(EKeyCode::Mouse1)
            || mInput->getKeyDown(EKeyCode::Mouse2))
        {
            onMouseDown();
        }

        auto mousePositionDelta = mInput->getMousePositionDelta();
        if(mousePositionDelta != Vector2(0,0))
        {
            onMouseMove();
        }

        if(
            mInput->getKeyUp(EKeyCode::Mouse0)
            || mInput->getKeyUp(EKeyCode::Mouse1)
            || mInput->getKeyUp(EKeyCode::Mouse2))
        {
            onMouseUp();
        }
        if(mInput->getMouseScrollDelta()!=Vector2())
        {
            onMouseWheel();
        }
    }

    bool _update()
    {
        auto position = this->mObject->getPosition();

        offset.copy( position ).sub( this->mTarget );

        // rotate offset to "y-axis-is-up" space
        offset.applyQuaternion( quat );

        // angle from z-axis around y-axis
        spherical.setFromVector3( offset );

        if ( this->autoRotate && state == STATE::NONE )
        {
            rotateLeft( getAutoRotationAngle() );
        }

        if ( this->enableDamping )
        {
            spherical.theta += sphericalDelta.theta * this->dampingFactor;
            spherical.phi += sphericalDelta.phi * this->dampingFactor;
        }
        else
        {
            spherical.theta += sphericalDelta.theta;
            spherical.phi += sphericalDelta.phi;
        }

        // restrict theta to be between desired limits

        auto min = this->minAzimuthAngle;
        auto max = this->maxAzimuthAngle;

        if ( std::numeric_limits<decltype(min)>::max() == min
            && std::numeric_limits<decltype(max)>::max() == max )
        {

            if ( min < - MathUtils::PI ) min += twoPI; else if ( min > MathUtils::PI ) min -= twoPI;

            if ( max < - MathUtils::PI ) max += twoPI; else if ( max > MathUtils::PI ) max -= twoPI;

            if ( min <= max ) {

                spherical.theta = std::max( min, std::min( max, spherical.theta ) );

            } else {

                spherical.theta = ( spherical.theta > ( min + max ) / 2 ) ?
                                      std::max( min, spherical.theta ) :
                                      std::min( max, spherical.theta );

            }

        }

        // restrict phi to be between desired limits
        spherical.phi = std::max( this->minPolarAngle, std::min( this->maxPolarAngle, spherical.phi ) );

        spherical.makeSafe();

        spherical.radius *= scale;

        // restrict radius to be between desired limits
        spherical.radius = std::max( this->minDistance, std::min( this->maxDistance, spherical.radius ) );

        // move target to panned location

        if ( this->enableDamping == true )
        {
            this->mTarget.add(
                Vector3(panOffset).multiplyScalar(this->dampingFactor)
            );
        }
        else
        {
            this->mTarget.add( panOffset );
        }

        offset.setFromSpherical( spherical );

        // rotate offset back to "camera-up-vector-is-up" space
        offset.applyQuaternion( quatInverse );

        position.copy( this->mTarget ).add( offset );
        this->mObject->setPosition(position);

        this->mObject->lookAt( this->mTarget );

        if ( this->enableDamping == true )
        {
            sphericalDelta.theta *= ( 1 - this->dampingFactor );
            sphericalDelta.phi *= ( 1 - this->dampingFactor );

            panOffset.multiplyScalar( 1 - this->dampingFactor );
        }
        else
        {

            sphericalDelta.set( 0, 0, 0 );
            panOffset.set( 0, 0, 0 );
        }
        scale = 1;

        // update condition is:
        // min(camera displacement, camera rotation in radians)^2 > EPS
        // using small-angle approximation cos(x/2) = 1 - x^2 / 8

        if ( zoomChanged ||
            lastPosition.distanceToSquared( this->mObject->getPosition() ) > EPS ||
            8 * ( 1 - lastQuaternion.dot( this->mObject->getQuaternion() ) ) > EPS ) {

//            this->dispatchEvent( _changeEvent );

            lastPosition = this->mObject->getPosition();
            lastQuaternion = this->mObject->getQuaternion();
            zoomChanged = false;

            return true;

        }

        return false;
    }

    // API
    Real movementSpeed = 10.0f;
    Real rollSpeed = 10.0f;

    bool dragToLook = false;
    bool autoForward = false;
protected:
    RefPtr<Node3D> mObject;
    RefPtr<IInput> mInput;

    // "target" sets the location of focus, where the object orbits around
    Vector3 mTarget;

    // How far you can dolly in and out ( PerspectiveCamera only )
    Real minDistance = 0.0f;
    Real maxDistance = MathUtils::MAX_VAL;

    // How far you can zoom in and out ( OrthographicCamera only )
    float minZoom = 0.0f;
    float maxZoom = MathUtils::MAX_VAL;

    // How far you can orbit vertically, upper and lower limits.
    // Range is 0 to Math.PI radians.
    float minPolarAngle = 0; // radians
    float maxPolarAngle = PI; // radians

    // How far you can orbit horizontally, upper and lower limits.
    // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI )
    Real minAzimuthAngle = MathUtils::MIN_VAL; // radians
    Real maxAzimuthAngle = MathUtils::MAX_VAL; // radians

    // Set to true to enable damping (inertia)
    // If damping is enabled, you must call controls.update() in your animation loop
    bool enableDamping = false;
    Real dampingFactor = 0.05;

    // This option actually enables dollying in and out; left as "zoom" for backwards compatibility.
    // Set to false to disable zooming
    bool enableZoom = true;
    Real zoomSpeed = 1.0;

    // Set to false to disable rotating
    bool enableRotate = true;
    Real rotateSpeed = 1.0;

    // Set to false to disable panning
    bool enablePan = true;
    Real panSpeed = 1.0;
    bool screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up
    Real keyPanSpeed = 7.0;	// pixels moved per arrow key push

    // Set to true to automatically rotate around the target
    // If auto-rotate is enabled, you must call controls.update() in your animation loop
    bool autoRotate = false;
    Real autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60

    // The four arrow keys
//    this.keys = { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' };

    // Mouse buttons
    enum mouseButtons
    {
        LEFT = MOUSE::ROTATE,
        MIDDLE= MOUSE::DOLLY,
        RIGHT= MOUSE::PAN,
    };

    // Touch fingers
//    this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN };

    // for reset
//    this.target0 = this.target.clone();
//    this.position0 = this.object.position.clone();
//    this.zoom0 = this.object.zoom;

    // the target DOM element for key events
//    this._domElementKeyEvents = null;
    Vector2 rotateStart;
    Vector2 rotateEnd;
    Vector2 rotateDelta;

    Vector2 panStart;
    Vector2 panEnd;
    Vector2 panDelta;

    Vector2 dollyStart;
    Vector2 dollyEnd;
    Vector2 dollyDelta;
};
POCELAIN_NAMESPACE_END
